import { BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import sharp from 'sharp'
import * as path from 'path'
import * as fs from 'fs/promises'
import { Stats } from 'fs'
import * as fsSync from 'fs'
import * as crypto from 'crypto'
import { LoggerUtil, LogContext } from './logger.util'
import { ExceptionUtil } from './exception.util'

/**
 * 文件类型枚举
 */
export enum FileType {
  IMAGE = 'image',
  DOCUMENT = 'document',
  VIDEO = 'video',
  AUDIO = 'audio',
  OTHER = 'other'
}

/**
 * 图片处理选项
 */
export interface ImageProcessOptions {
  width?: number
  height?: number
  quality?: number
  format?: 'jpeg' | 'png' | 'webp' | 'avif'
}

/**
 * 文件验证选项
 */
export interface FileValidationOptions {
  maxSize?: number
  allowedTypes?: string[]
  allowedExtensions?: string[]
}

/**
 * 文件信息接口
 */
export interface FileInfo {
  originalName: string
  filename: string
  path: string
  mimeType: string
  size: number
  type: FileType
  url?: string
}

/**
 * 统一文件处理工具类
 * 提供文件验证、处理、存储等通用功能
 */
export class FileUtil {
  private static readonly logger = new Logger(FileUtil.name);
  private static readonly DEFAULT_MAX_SIZE = 10 * 1024 * 1024 // 10MB
  private static readonly ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']
  private static readonly ALLOWED_DOCUMENT_TYPES = [
    'application/pdf',
    'application/msword',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    'application/vnd.ms-excel',
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    'text/plain',
    'text/csv'
  ]
  private static readonly ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/avi', 'video/mov', 'video/wmv']
  private static readonly ALLOWED_AUDIO_TYPES = ['audio/mp3', 'audio/wav', 'audio/ogg', 'audio/m4a']

  /**
   * 验证文件
   * @param file 文件对象
   * @param options 验证选项
   * @param context 日志上下文
   */
  static validateFile(file: Express.Multer.File, options: FileValidationOptions = {}, context?: LogContext): void {
    try {
      if (!file) {
        throw ExceptionUtil.createHttpException('未选择文件', 400)
      }

      if (!file.originalname) {
        throw ExceptionUtil.createHttpException('文件名无效', 400)
      }

      // 验证文件大小
      const maxSize = options.maxSize || this.DEFAULT_MAX_SIZE
      if (file.size > maxSize) {
        throw ExceptionUtil.createHttpException(`文件大小超过限制 (${Math.round(maxSize / 1024 / 1024)}MB)`, 400)
      }

      // 验证MIME类型
      if (options.allowedTypes && !options.allowedTypes.includes(file.mimetype)) {
        throw ExceptionUtil.createHttpException('不支持的文件类型', 400)
      }

      // 验证文件扩展名
      if (options.allowedExtensions) {
        const ext = path.extname(file.originalname).toLowerCase()
        if (!options.allowedExtensions.includes(ext)) {
          throw ExceptionUtil.createHttpException('不支持的文件扩展名', 400)
        }
      }

      LoggerUtil.fileOperation(FileUtil.logger, '文件验证', file.originalname, file.size, context)
    } catch (error) {
      LoggerUtil.fileOperation(FileUtil.logger, '文件验证失败', file?.originalname || 'unknown', file?.size, context)
      throw error
    }
  }

  /**
   * 确定文件类型
   * @param mimeType MIME类型
   * @returns 文件类型
   */
  static determineFileType(mimeType: string): FileType {
    if (this.ALLOWED_IMAGE_TYPES.includes(mimeType)) {
      return FileType.IMAGE
    }
    if (this.ALLOWED_DOCUMENT_TYPES.includes(mimeType)) {
      return FileType.DOCUMENT
    }
    if (mimeType.startsWith('video/') || this.ALLOWED_VIDEO_TYPES.includes(mimeType)) {
      return FileType.VIDEO
    }
    if (mimeType.startsWith('audio/') || this.ALLOWED_AUDIO_TYPES.includes(mimeType)) {
      return FileType.AUDIO
    }
    return FileType.OTHER
  }

  /**
   * 生成唯一文件名
   * @param originalName 原始文件名
   * @param preserveExtension 是否保留扩展名
   * @returns 新文件名
   */
  static generateFileName(originalName: string, preserveExtension = true): string {
    const timestamp = Date.now()
    const random = Math.random().toString(36).substring(2, 8)
    const ext = preserveExtension ? path.extname(originalName) : ''
    return `${timestamp}_${random}${ext}`
  }

  /**
   * 获取文件存储路径
   * @param type 文件类型
   * @param fileName 文件名
   * @param useDate 是否使用日期分组
   * @returns 相对路径
   */
  static getStoragePath(type: FileType, fileName: string, useDate = true): string {
    if (!useDate) {
      return path.join(type, fileName)
    }

    const date = new Date()
    const year = date.getFullYear()
    const month = String(date.getMonth() + 1).padStart(2, '0')
    const day = String(date.getDate()).padStart(2, '0')

    return path.join(type, year.toString(), month, day, fileName)
  }

  /**
   * 确保目录存在
   * @param dirPath 目录路径
   * @param context 日志上下文
   */
  static async ensureDirectory(dirPath: string, context?: LogContext): Promise<void> {
    try {
      await fs.access(dirPath)
    } catch {
      await fs.mkdir(dirPath, { recursive: true })
      LoggerUtil.fileOperation(FileUtil.logger, '创建目录', dirPath, undefined, context)
    }
  }

  /**
   * 保存文件到磁盘
   * @param buffer 文件缓冲区
   * @param filePath 文件路径
   * @param context 日志上下文
   */
  static async saveFile(buffer: Buffer, filePath: string, context?: LogContext): Promise<void> {
    try {
      // 确保目录存在
      await this.ensureDirectory(path.dirname(filePath), context)

      // 保存文件
      await fs.writeFile(filePath, buffer)

      LoggerUtil.fileOperation(this.logger, '保存文件', filePath, buffer.length, context)
    } catch (error) {
      LoggerUtil.fileOperation(this.logger, '保存文件', filePath, undefined, context)
      throw ExceptionUtil.handleFileError(error, '保存文件失败')
    }
  }

  /**
   * 删除文件
   * @param filePath 文件路径
   * @param context 日志上下文
   * @param throwOnError 是否在错误时抛出异常
   */
  static async deleteFile(filePath: string, context?: LogContext, throwOnError = false): Promise<boolean> {
    try {
      await fs.unlink(filePath)
      LoggerUtil.fileOperation(this.logger, '删除文件', filePath, undefined, context)
      return true
    } catch (error) {
      LoggerUtil.fileOperation(this.logger, '删除文件', filePath, undefined, context)
      if (throwOnError) {
        throw ExceptionUtil.handleFileError(error, '删除文件失败')
      }
      return false
    }
  }

  /**
   * 检查文件是否存在
   * @param filePath 文件路径
   * @returns 是否存在
   */
  static async fileExists(filePath: string): Promise<boolean> {
    try {
      await fs.access(filePath)
      return true
    } catch {
      return false
    }
  }

  /**
   * 获取文件信息
   * @param filePath 文件路径
   * @returns 文件统计信息
   */
  static async getFileStats(filePath: string): Promise<Stats | null> {
    try {
      return await fs.stat(filePath)
    } catch {
      return null
    }
  }

  /**
   * 处理图片
   * @param buffer 图片缓冲区
   * @param options 处理选项
   * @param context 日志上下文
   * @returns 处理后的图片缓冲区
   */
  static async processImage(buffer: Buffer, options: ImageProcessOptions = {}, context?: LogContext): Promise<Buffer> {
    try {
      let image = sharp(buffer)

      // 调整尺寸
      if (options.width || options.height) {
        image = image.resize(options.width, options.height, {
          fit: 'inside',
          withoutEnlargement: true
        })
      }

      // 设置格式和质量
      const format = options.format || 'jpeg'
      const quality = options.quality || 80

      switch (format) {
        case 'jpeg':
          image = image.jpeg({ quality })
          break
        case 'png':
          image = image.png({ quality })
          break
        case 'webp':
          image = image.webp({ quality })
          break
        case 'avif':
          image = image.avif({ quality })
          break
        default:
          image = image.jpeg({ quality })
      }

      const result = await image.toBuffer()
      LoggerUtil.fileOperation(this.logger, '图片处理', `${options.width}x${options.height}`, undefined, context)
      return result
    } catch (error) {
      LoggerUtil.fileOperation(this.logger, '图片处理', 'unknown', undefined, context)
      throw ExceptionUtil.handleFileError(error, '图片处理失败')
    }
  }

  /**
   * 获取MIME类型对应的文件扩展名
   * @param mimeType MIME类型
   * @returns 文件扩展名
   */
  static getExtensionFromMimeType(mimeType: string): string {
    const mimeToExt: Record<string, string> = {
      'image/jpeg': '.jpg',
      'image/png': '.png',
      'image/webp': '.webp',
      'image/gif': '.gif',
      'image/avif': '.avif',
      'application/pdf': '.pdf',
      'text/plain': '.txt',
      'text/csv': '.csv',
      'application/json': '.json',
      'video/mp4': '.mp4',
      'video/avi': '.avi',
      'audio/mp3': '.mp3',
      'audio/wav': '.wav'
    }

    return mimeToExt[mimeType] || ''
  }

  /**
   * 根据格式获取MIME类型
   * @param format 文件格式
   * @returns MIME类型
   */
  static getMimeTypeFromFormat(format: string): string {
    const formatToMime: Record<string, string> = {
      jpeg: 'image/jpeg',
      jpg: 'image/jpeg',
      png: 'image/png',
      webp: 'image/webp',
      gif: 'image/gif',
      avif: 'image/avif',
      pdf: 'application/pdf',
      txt: 'text/plain',
      csv: 'text/csv',
      json: 'application/json',
      mp4: 'video/mp4',
      avi: 'video/avi',
      mp3: 'audio/mp3',
      wav: 'audio/wav'
    }

    return formatToMime[format.toLowerCase()] || 'application/octet-stream'
  }

  /**
   * 生成文件URL
   * @param relativePath 相对路径
   * @param baseUrl 基础URL
   * @returns 完整的文件URL
   */
  static generateFileUrl(relativePath: string, baseUrl: string): string {
    const normalizedPath = relativePath.replace(/\\/g, '/')
    return `${baseUrl.replace(/\/$/, '')}/uploads/${normalizedPath}`
  }

  /**
   * 递归获取目录下所有文件
   * @param dirPath 目录路径
   * @param extensions 文件扩展名过滤
   * @returns 文件路径数组
   */
  static getAllFiles(dirPath: string, extensions?: string[]): string[] {
    const files: string[] = []

    if (!fsSync.existsSync(dirPath)) {
      return files
    }

    const items = fsSync.readdirSync(dirPath)

    for (const item of items) {
      const fullPath = path.join(dirPath, item)
      const stat = fsSync.statSync(fullPath)

      if (stat.isDirectory()) {
        files.push(...this.getAllFiles(fullPath, extensions))
      } else {
        if (!extensions || extensions.includes(path.extname(item).toLowerCase())) {
          files.push(fullPath)
        }
      }
    }

    return files
  }

  /**
   * 计算文件哈希值
   * @param buffer 文件缓冲区
   * @param algorithm 哈希算法
   * @returns 哈希值
   */
  static calculateHash(buffer: Buffer, algorithm = 'md5'): string {
    return crypto.createHash(algorithm).update(buffer).digest('hex')
  }

  /**
   * 格式化文件大小
   * @param bytes 字节数
   * @param decimals 小数位数
   * @returns 格式化后的大小字符串
   */
  static formatFileSize(bytes: number, decimals = 2): string {
    if (bytes === 0) return '0 Bytes'

    const k = 1024
    const dm = decimals < 0 ? 0 : decimals
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']

    const i = Math.floor(Math.log(bytes) / Math.log(k))

    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
  }

  /**
   * 验证图片文件
   * @param file 文件对象
   * @param context 日志上下文
   */
  static validateImageFile(file: Express.Multer.File, context?: LogContext): void {
    this.validateFile(
      file,
      {
        allowedTypes: this.ALLOWED_IMAGE_TYPES,
        maxSize: 5 * 1024 * 1024 // 5MB for images
      },
      context
    )
  }

  /**
   * 验证文档文件
   * @param file 文件对象
   * @param context 日志上下文
   */
  static validateDocumentFile(file: Express.Multer.File, context?: LogContext): void {
    this.validateFile(
      file,
      {
        allowedTypes: this.ALLOWED_DOCUMENT_TYPES,
        maxSize: 20 * 1024 * 1024 // 20MB for documents
      },
      context
    )
  }
}